Skip to content

[ET-VK][tools] Write etvk_ops_bench JSON files compactly - #24806

Merged
meta-codesync[bot] merged 3 commits into
gh/SS-JIA/697/basefrom
gh/SS-JIA/697/head
Nov 10, 2026
Merged

meta-codesync[bot] merged 3 commits into
gh/SS-JIA/697/basefrom
gh/SS-JIA/697/head

Conversation

@SS-JIA

@SS-JIA SS-JIA commented Nov 9, 2026 •

Copy link
Copy Markdown
Contributor

Stack from ghstack (oldest at bottom):

Ulterior Motive

Keeping etvk_ops_bench JSON files reviewable. convolution/conv2d/cases/correctness.json is 8,445 lines, mostly whitespace, and the conv2d epilogue correctness cases would grow it further.

Rationale

What:

  • Adds format_json.py to benchmark_v2. It keeps a JSON value on one line when its one-line form, not counting its key and indentation, fits in 200 characters, and lists longer objects and arrays one entry per line. An object with one entry is measured by that entry's value, so a {"tensor": {...}} wrapper never splits on its own. In operator cases, each case key and each operator argument gets one line.
  • Reformats every JSON file under benchmark_v2 with it: the 64 operator case files and the 13 examples.
  • Adds an ETVKJSONFORMAT lintrunner linter for these files. lintrunner reports unformatted or invalid files, and lintrunner -a formats them.
  • Replaces fb/scripts/apply_prettier.sh, whose Prettier layout would expand the files again. Both READMEs are updated.

Why: The previous layout put every tensor field and every scalar wrapper on its own line, so one 17-argument set took about 95 lines. The JSON files shrink from 87,631 to 23,727 lines with the same content; conv2d correctness.json goes from 8,445 to 2,214.

Details

  • Content is unchanged: every file parses to the same values, types and key order as before. Besides whitespace, 4 files re-spell float exponents (e38 → e+38, e-7 → e-07), which parse to the same values.
  • Lint: lintrunner is a required check on GitHub PRs (the lintrunner job in lint.yml, which lints the PR's changed files) and the lint developers run before submitting. The linter runs format_json.py --lintrunner, which prints one lintrunner message per file instead of writing it:
    • unformatted files get a warning with the formatted contents as the replacement;
    • files that cannot be formatted get an error.
  • Rejected files: invalid UTF-8, duplicate keys, NaN / Infinity values, numbers outside float range (such as 1e400), and strings with no UTF-8 encoding (lone \ud800 escapes). Without --lintrunner, the script exits with the file and the error, before writing that file.
  • The script writes UTF-8 with \n line endings.
  • Example operator argument set:
"args_sweep": [
  [
    {"tensor": {"name": "input", "sizes": [1, 8, 8, 8], "dtype": "float32", "storage": "texture3d", "layout": "channels", "data": {"rand_uniform_float": [-1, 1]}}},
    {"tensor": {"name": "weight", "sizes": [8, 8, 3, 3], "dtype": "float32", "storage": "constant", "layout": "channels", "data": {"rand_uniform_float": [-1, 1]}}},
    null,
    {"int_list": [1, 1]},
    ...
  ]
]
  • Longer values, such as long v_SIZES sweep lists, list one entry per line.
  • In-flight diffs that edit these files will conflict. Reformatting their side with format_json.py before resolving leaves only the real edits in the conflict. Line blame for the JSON files points at this commit.

Usage, from the ExecuTorch root:

lintrunner                                                                # reports unformatted JSON files among the changed files
lintrunner -a                                                             # formats them
python3 backends/vulkan/test/ops/benchmark_v2/format_json.py              # every JSON file under benchmark_v2
python3 backends/vulkan/test/ops/benchmark_v2/format_json.py PATH [...]   # given files, or JSON files under given directories

Differential Revision: D130231969

[ghstack-poisoned]
@pytorch-bot

pytorch-bot Bot commented Nov 9, 2026 •

Copy link
Copy Markdown

🔗 Helpful Links

🧪 See artifacts and rendered test results at hud.pytorch.org/pr/pytorch/executorch/24806

Note: Links to docs will display an error until the docs builds have been completed.

✅ No Failures

As of commit 057066d with merge base e5ffebe (image):
💚 Looks good so far! There are no failures yet. 💚

This comment was automatically generated by Dr. CI and updates every 15 minutes.

@github-actions

github-actions Bot commented Nov 9, 2026

Copy link
Copy Markdown

This PR needs a release notes: label

If your change should be included in the release notes (i.e. would users of this library care about this change?), please use a label starting with release notes:. This helps us keep track and include your important work in the next release notes.

To add a label, you can comment to pytorchbot, for example
@pytorchbot label "release notes: none"

For more information, see
https://github.com/pytorch/pytorch/wiki/PyTorch-AutoLabel-Bot#why-categorize-for-release-notes-and-how-does-it-work.

[ghstack-poisoned]
[ghstack-poisoned]
@meta-codesync
meta-codesync Bot merged commit 94e3a88 into gh/SS-JIA/697/base Nov 10, 2026
212 of 213 checks passed
@meta-codesync
meta-codesync Bot deleted the gh/SS-JIA/697/head branch November 10, 2026 02:40
@meta-codesync
meta-codesync Bot deployed to cherry-pick-bot November 10, 2026 02:40 Active
meta-codesync Bot pushed a commit that referenced this pull request Nov 10, 2026
…aders

Stack from [ghstack](https://github.com/ezyang/ghstack/tree/0.15.0) (oldest at bottom):
* #24813
* #24808
* __->__ #24807
* #24806

## Ulterior Motive

Closing the DSR FP16 gap to LiteRT. DSR has 83 skip connections of the form `conv(...) + x * scale`. ET-VK runs each as a separate `mul` and `add` (168 dispatches, 347 ms per inference on Galaxy S25 against LiteRT's 148 ms). This commit adds runtime support for fusing them into the preceding convolution; the next commit fuses them during lowering.

## Rationale

**What**: Every conv2d shader (direct, pointwise, depthwise, im2col GEMM and transposed) gains residual and scale bindings and applies an epilogue before storing:

```
out = activation(residual_op(conv + bias, residual * scale, alpha))
```

Specialization constants select each stage. A new `et_vk.conv2d.with_epilogue` op exposes it.

**Why**: One dispatch replaces conv, `mul` and `add`, and the conv output and scaled residual are never written to memory. Because every route applies the epilogue itself, the op never falls back to separate element-wise dispatches.

## Details

- **Epilogue** (`conv2d_epilogue.glslh`): calls `apply_binary_op()` for the residual op and `apply_unary_op()` for the activation.
  - The C++ side accepts add, sub and mul residual ops and clamp, relu and leaky_relu activations.
  - Inlining the `apply_unary_op()` switch changes Adreno's code generation for the 2x2 tiled direct shaders, even with the activation specialized away: AOC instruction counts rise 6–25% (`conv2d_2x2x8_border_float`: 532 → 660) with unchanged register counts.
  - Against a switch limited to the three supported activations, large FP16 3x3 and 5x5 direct convolutions are 2.1–2.9% slower on Pixel 8 Pro, and small 3x3 direct convolutions 4.4–5.7% slower on Galaxy S23. Calling `apply_unary_op()` avoids a second activation dispatcher.
- **Absent operands**: The residual slot binds the convolution input and the scale slot binds the packed bias, which have the slots' storage types. `has_residual` / `has_scale` gate the reads.
- **Scale**: A constant `[1, C, 1, 1]` or `[C, 1, 1]` residual scale. Each route prepacks it like the bias (texture2d, using the transposed weight layout for transposed convolutions) with `prepack_conv2d_residual_scale()`, next to the bias prepack, before building the bindings (`Conv2dEpilogueArgs`).
- **Validation**: `conv2d_epilogue_from_args()` throws at graph build when:
  - the residual is not texture3d or does not match the output's sizes, dtype and layout;
  - the scale is not a constant, is given without a residual, does not have the weight's dtype, or is not `[1, C, 1, 1]` or `[C, 1, 1]`. Other shapes with C values, such as `[C]`, broadcast along another dim in PyTorch;
  - the residual op or activation is unsupported.
  conv2d nodes pass the residual as their last resize argument, and their resize functions check that it still matches the resized output's sizes.
- **Clamp**
  - **Before**: Each shader family selected its clamp differently: a `clamp_out` spec constant and an `out_min` / `out_max` UBO (direct); an `activation_type` 0/1/2 spec constant (pointwise and im2col GEMM); separately generated `_clamp` shader variants (depthwise and transposed).
  - **After**: All use `has_activation` + `activation_op` with `activation_a` / `activation_b`. The `_clamp` variants are gone. `conv_with_clamp` builds a clamp epilogue.
- **Bindings and constants**
  - `conv2d_tiled`, `conv2d_dw` and `conv_transpose2d`: residual at 4, scale at 5, UBOs move from 4–8 to 6–10. UBO 10 holds `activation_a`, `activation_b` and `residual_alpha`.
  - `conv2d_pw_tiled`, `conv2d_gemm` and the depthwise output-tile shaders: residual and scale follow the bias; the clamp push constants become `activation_a`, `activation_b` and `residual_alpha`.
  - The epilogue specialization constants `has_activation`, `activation_op`, `has_residual`, `has_scale` and `residual_op` are contiguous in every shader, and `append_conv2d_epilogue_spec_vars()` appends all five. In `conv2d_tiled`, `has_bias` moves from ID 7 to 6, the group channel counts from 8–9 to 7–8, and `has_activation` (formerly `clamp_out`, 6) takes 9, ahead of the other epilogue constants at 10–13.
  - The unused `ngroups` (`conv2d_dw`) and `packed_dim` (`conv_transpose2d`) spec constants are removed. The transposed convolution no longer needs the `has_activation` resize argument that picked its `_clamp` variant.
- **Entry point**: `conv2d_with_epilogue_impl()` replaces `conv2d_impl()` and implements `aten.convolution`, `conv_with_clamp` and `conv2d.with_epilogue` for 4D inputs.
- **Depthwise tile override**: `conv2d_dw_impl` takes an optional `b1x1_override`. The benchmark's depthwise `b1x1` / `b4x2` routes call it instead of carrying a copy of the depthwise dispatch. The `b1x1` route now also selects the 1x1 tile for 5x5 kernels, as the Mali default already does.
- **Benchmark**
  - Adds a `conv2d_with_epilogue` reference and a transposed convolution reference; the conv2d reference previously rejected transposed convolutions. The epilogue reference rejects the scale shapes and the scale without a residual that the runtime rejects.
  - `et_vk.conv2d.with_epilogue` accepts the `direct`, `im2col`, `b1x1` and `b4x2` routes and reports throughput.
  - Adds 204 correctness cases. 200 cover 5 epilogues × FP32/FP16 on 20 shape and route combinations:
    - one shape per route: direct, pointwise, im2col, the three depthwise shaders, transposed;
    - partial output texels or tiles on the direct, pointwise, im2col, depthwise tile and transposed routes;
    - a strided and dilated direct conv and a grouped direct conv.
  - The other 4 run a pointwise, direct, im2col and depthwise epilogue through a full → shrunk → full resize.
  - Adds the `operator_dynamic_resize` case check. It builds an operator case, which must expand to one case, once, then for each step resizes the staged inputs, checks the output sizes and validates the output. The q8ta streaming-shrink check now uses it instead of its own copy.
  - The depthwise `b1x1` / `b4x2` routes report batch > 1 as unsupported (SKIP), since the depthwise shaders support batch 1 only.
  - The route inspector now recognizes the tiled direct shader names.
  - `conv2d_gemm_sustained.json` binds the new GEMM slots.

Usage:

```cpp
// Parses and validates the epilogue arguments of a conv2d op
const Conv2dEpilogue epilogue = conv2d_epilogue_from_args(graph, args);
conv2d_direct_impl(graph, in, weight, bias, stride, padding, dilation,
                   transposed, output_padding, groups, out, epilogue);

// Inside a route
const ValueRef packed_scale =
    prepack_conv2d_residual_scale(graph, epilogue, weight, transposed);
const Conv2dEpilogueArgs epilogue_args(epilogue, in, packed_bias, packed_scale);
// Bind epilogue_args.residual and epilogue_args.scale, append the epilogue's
// specialization constants, and pass epilogue.residual as the last resize arg.
append_conv2d_epilogue_spec_vars(spec_vars, epilogue);
```

Differential Revision: D130216287

Pull Request resolved: #24807
meta-codesync Bot pushed a commit that referenced this pull request Nov 10, 2026
… lowering

Stack from [ghstack](https://github.com/ezyang/ghstack/tree/0.15.0) (oldest at bottom):
* #24813
* __->__ #24808
* #24807
* #24806

## Ulterior Motive

Closing the DSR FP16 gap to LiteRT. DSR has 83 skip connections of the form `conv(...) + x * scale`, each lowered today to a separate `mul` and `add` dispatch. The previous commit added a conv2d epilogue that applies them inside every conv2d shader; this commit makes lowering use it.

It also makes `et_vk.conv2d.with_epilogue` the single fused op for conv2d, replacing `et_vk.conv_with_clamp` there.

## Rationale

**What**: A `conv2d_with_epilogue` pattern for `FusePatternsPass` that rewrites either of

```
activation(residual_op(convolution(...), residual [* scale]))
activation(convolution(...))
```

into `et_vk.conv2d.with_epilogue`, plus the op's definition and `op_registry` entry. A new `skip_fusion_patterns` compile option lets `WebGPUPartitioner` skip the pattern.

**Why**: Without the pattern, no exported model reaches the epilogue. On DSR, an earlier revision of this stack shrank the Vulkan delegate from 469 to 229 operators and cut end-to-end latency 9.6% on Galaxy S23 and Pixel 8 Pro. The WebGPU runtime consumes Vulkan lowering but does not implement `et_vk.conv2d.with_epilogue`.

## Details

- **Behavior**
  - conv2d + activation
    - **Before**: `FuseClampPass` rewrote it into `et_vk.conv_with_clamp`.
    - **After**: `et_vk.conv2d.with_epilogue`. At runtime both reach `add_conv2d` with an activation epilogue.
  - conv2d + residual, with or without an activation
    - **Before**: separate `aten.convolution`, `mul`, `add` and activation ops.
    - **After**: one `et_vk.conv2d.with_epilogue`.
  - conv1d + activation: unchanged. `Conv1dAsConv2dPass` and `FuseClampPass` still run after `FusePatternsPass`, so conv1d, including conv1d rewritten as conv2d, fuses into `et_vk.conv_with_clamp`, which stays registered for conv1d, WebGPU and existing `.pte` files.
  - WebGPU lowering: unchanged. `WebGPUPartitioner` skips `conv2d_with_epilogue`, so conv2d + activation still becomes `et_vk.conv_with_clamp`.
- **Match**
  - A 2D, batch 1, FP16/FP32 `aten.convolution` (direct, pointwise, depthwise, grouped or transposed) with one user, whose input and output fit channels-packed textures.
  - Feeding `aten.add.Tensor`, `aten.sub.Tensor` or `aten.mul.Tensor` with a residual of the conv output's shape and dtype. The conv may be either operand of `mul`, and either operand of `add` when `alpha` is 1; it must be the first operand of `sub`.
  - When the residual is a `mul` of `x` and a `[1, C, 1, 1]` or `[C, 1, 1]` placeholder `scale`, and the residual op is the `mul`'s only user, `x` becomes the residual and `scale` the residual scale. The runtime binds the residual as a tensor and prepacks the scale, so when `scale` is not a constant or `x` is one, the `mul` stays separate and its output is the residual.
  - The residual cannot be the conv itself (`y + y` stays unfused), and a constant residual without a scale stays unfused.
  - `relu`, `hardtanh`, `clamp` or `leaky_relu` is fused as the activation when it is the only user of the binary op, or of the conv when there is no residual. A missing `hardtanh` bound defaults to -1 or 1. Activations with non-constant parameters, such as a `clamp` bound by a symbolic size, stay unfused.
- **Partitioning**: As for the other patterns, `VulkanPartitioner` delegates a match's nodes without checking their support. The detector therefore repeats the conv's own checks (rank, batch, dtype, texture limits), so a conv that exceeds the texture limits stays on CPU unfused.
- **Pattern replacement**
  - `replace_all_fusable_subgraphs` removes dead code after the detectors replace a match at a node, so nodes another pattern already replaced (for example the conv and relu of a quantized conv) cannot match again.
  - A replacement function may return `False` when it leaves the graph unchanged, as this pattern does for a constant residual. Such matches are not counted, so `FusePatternsPass` does not retrace the graph for them.
- **Registry**: The residual is channels-packed texture3d like the input and output; weight, bias and scale are prepacked.
- **`skip_fusion_patterns`**: a collection of pattern names; a string or an unknown name raises `ValueError`. It is serialized as a compile spec, and both the partitioner's pattern detection and `FusePatternsPass` honor it:

```python
VulkanPartitioner(compile_options={"skip_fusion_patterns": {"conv2d_with_epilogue"}})
```

- **Compatibility**: `.pte` files exported after this commit contain `et_vk.conv2d.with_epilogue` for every FP conv2d followed by an activation or a residual op, so they need a runtime that includes the previous commit. Existing `.pte` files keep loading, since `et_vk.conv_with_clamp` stays registered.
- **WebGPU tests**: The op test generator and `test_conv_with_clamp.py` lowered through `VulkanPartitioner` directly; they now use `WebGPUPartitioner`.

```
schema: conv2d.with_epilogue(Tensor input, Tensor weight, Tensor? bias, SymInt[] stride,
  SymInt[] padding, SymInt[] dilation, bool transposed, SymInt[] output_padding,
  SymInt groups, Tensor? residual, Tensor? residual_scale, str residual_op,
  Scalar alpha, str activation, float activation_a, float activation_b) -> Tensor
```

Differential Revision: D130216290

Pull Request resolved: #24808
meta-codesync Bot pushed a commit that referenced this pull request Nov 10, 2026
Stack from [ghstack](https://github.com/ezyang/ghstack/tree/0.15.0) (oldest at bottom):
* __->__ #24813
* #24808
* #24807
* #24806

## Ulterior Motive

RIFE's conv + residual + LeakyReLU blocks now lower to `et_vk.conv2d.with_epilogue`, which replaces the explicit `conv2d_residual_lrelu` / `conv2d_lrelu` ops. On Galaxy S23, direct convs with the generic epilogue ran 5–10% slower than with hardcoded `leaky_relu(conv + residual)` math. The leaky_relu formula accounts for most of that gap.

## Rationale

**What**: `leaky_relu(vec4)` now selects between `x` and `x * slope` with one `mix`. The scalar overload, now unused, is removed.

**Why**: The old form evaluated `x * (float(x > 0) + slope * float(x <= 0))` once per component: two compares, two conversions and two multiplies per lane. The conv2d epilogue applies it to every output texel, and the unary operators share the header.

## Details

- Outputs are identical for finite slopes, including at ±0, ±inf and NaN.
- On the RIFE residual configuration, the conv2d SPIR-V drops to:
  - Adreno: about 96 ALU instructions;
  - Mali-G715: 0.5 arithmetic cycles, about 6%.

Differential Revision: D130740220

Pull Request resolved: #24813
SS-JIA added a commit that referenced this pull request Nov 10, 2026
Pull Request resolved: #24806

## Ulterior Motive

Keeping `etvk_ops_bench` JSON files reviewable. `convolution/conv2d/cases/correctness.json` is 8,445 lines, mostly whitespace, and the conv2d epilogue correctness cases would grow it further.

## Rationale

**What**:
- Adds `format_json.py` to `benchmark_v2`. It keeps a JSON value on one line when its one-line form, not counting its key and indentation, fits in 200 characters, and lists longer objects and arrays one entry per line. An object with one entry is measured by that entry's value, so a `{"tensor": {...}}` wrapper never splits on its own. In operator cases, each case key and each operator argument gets one line.
- Reformats every JSON file under `benchmark_v2` with it: the 64 operator case files and the 13 examples.
- Adds an `ETVKJSONFORMAT` lintrunner linter for these files. `lintrunner` reports unformatted or invalid files, and `lintrunner -a` formats them.
- Replaces `fb/scripts/apply_prettier.sh`, whose Prettier layout would expand the files again. Both READMEs are updated.

**Why**: The previous layout put every tensor field and every scalar wrapper on its own line, so one 17-argument set took about 95 lines. The JSON files shrink from 87,631 to 23,727 lines with the same content; conv2d `correctness.json` goes from 8,445 to 2,214.

## Details

- Content is unchanged: every file parses to the same values, types and key order as before. Besides whitespace, 4 files re-spell float exponents (`e38` → `e+38`, `e-7` → `e-07`), which parse to the same values.
- **Lint**: lintrunner is a required check on GitHub PRs (the `lintrunner` job in `lint.yml`, which lints the PR's changed files) and the lint developers run before submitting. The linter runs `format_json.py --lintrunner`, which prints one lintrunner message per file instead of writing it:
  - unformatted files get a warning with the formatted contents as the replacement;
  - files that cannot be formatted get an error.
- **Rejected files**: invalid UTF-8, duplicate keys, `NaN` / `Infinity` values, numbers outside float range (such as `1e400`), and strings with no UTF-8 encoding (lone `\ud800` escapes). Without `--lintrunner`, the script exits with the file and the error, before writing that file.
- The script writes UTF-8 with `\n` line endings.
- Example operator argument set:

```json
"args_sweep": [
  [
    {"tensor": {"name": "input", "sizes": [1, 8, 8, 8], "dtype": "float32", "storage": "texture3d", "layout": "channels", "data": {"rand_uniform_float": [-1, 1]}}},
    {"tensor": {"name": "weight", "sizes": [8, 8, 3, 3], "dtype": "float32", "storage": "constant", "layout": "channels", "data": {"rand_uniform_float": [-1, 1]}}},
    null,
    {"int_list": [1, 1]},
    ...
  ]
]
```

- Longer values, such as long `v_SIZES` sweep lists, list one entry per line.
- In-flight diffs that edit these files will conflict. Reformatting their side with `format_json.py` before resolving leaves only the real edits in the conflict. Line blame for the JSON files points at this commit.

Usage, from the ExecuTorch root:

```sh
lintrunner                                                                # reports unformatted JSON files among the changed files
lintrunner -a                                                             # formats them
python3 backends/vulkan/test/ops/benchmark_v2/format_json.py              # every JSON file under benchmark_v2
python3 backends/vulkan/test/ops/benchmark_v2/format_json.py PATH [...]   # given files, or JSON files under given directories
```
ghstack-source-id: 469036179
@exported-using-ghexport

Differential Revision: [D130231969](https://our.internmc.facebook.com/intern/diff/D130231969/)

This branch was successfully deployed

1 active and 1 inactive deployments
cherry-pick-bot — 057066df Deployed Nov 10, 2026 by meta-codesync[bot] via Try to create a PR with ghstack /orig branch #1927
cadence — 057066df Deployed Nov 9, 2026 by SS-JIA via hifi-op-test / hifi4 #37491
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. meta-exported

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants